Skip to content

lfs: extract the Batch API into an importable module - #23

Open
unknwon wants to merge 3 commits into
mainfrom
lfs-library
Open

unknwon wants to merge 3 commits into
mainfrom
lfs-library

Conversation

@unknwon

@unknwon unknwon commented Nov 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

Moves the LFS logic of lfsd, including its HTTP handling, into a separate Go module, unknwon.dev/git-lfs-server/lfs, so Gogs (and other Git servers) can serve Git LFS with the same storage backends, including S3-compatible presigned storage.

  • lfs.Service.Serve handles the batch, upload, download, and verify requests of a repository. The caller routes and authenticates the request, then passes an lfs.Access (repository key, base URL of its endpoints, read and write permission). It dispatches on the method and the last path segment, so it works with any router that runs net/http handlers (Flamego, Macaron, http.ServeMux) and any URL layout.
  • Serve sits on Batch, Upload, Download, and Verify, which stay exported for servers that need their own HTTP handling. They work on an lfs.Store (metadata) and a storage.Backend (content).
  • lfs/storage (moved from internal/storage) and lfs/internal/iox (moved from internal/iox) ship with the module. URI(oid) moved from Presigner up to Backend.
  • Service.PreviousStorages keeps objects readable after the backend for new uploads changes, e.g., local disk to S3.
  • Service.CrossRepoDedup is on in lfsd (current behavior). Servers whose metadata cannot share objects across repositories leave it off.
  • It is a separate module with go 1.26, so importers do not pull in PostgreSQL, Flamego, or the forge clients, and do not need the Go version of lfsd. CI, Dockerfile, and moon cover both modules.

lfsd builds one lfs.Service per forge host, maps forge permissions to lfs.Access, and internal/database.DB implements lfs.Store.

Behavior changes in lfsd

  • A proxied (filesystem) upload of an already stored OID now has to send matching content before the object is linked to the uploading repository. Before, FilesystemBackend.Put returned early without reading the body, so any writer of one repository could link any known OID and size to their repository with arbitrary bytes and then download the original content. Gogs fixed the same issue in its own LFS code. Presigned uploads were not affected.
  • Upload actions set Content-Type: application/octet-stream, so the Git LFS client does not send a type detected from the file content. Gogs requires it on uploads, and it is not part of the presigned signature.
  • Log lines from LFS requests are scoped [lfs] instead of [batch], [upload], [download], and [verify].

Test plan

  • moon run :lint :test for both projects, and go test -race in lfs.
  • lfs/service_test.go covers proxy and presign flows against the real filesystem and gofakes3 backends. Mutation checks: removing the body drain, or forcing cross-repo dedup on, fails the tests.
  • lfs/http_test.go covers Serve: dispatch, permission checks, status codes, the error format, credential forwarding into actions, and redirects for presigned storage.
  • 48-case HTTP probe (curl) against lfsd on main and on this branch, on PostgreSQL 17 with filesystem and gofakes3 storage. Status codes, headers, and bodies match except the two changes above.
  • Real git-lfs 3.6.1 push and clone through lfsd for filesystem and S3 presign.
  • Built Gogs on Serve through a local replace (Gogs changes not in this PR). Objects written by the old Gogs still download, local and S3 round trips pass, and with S3 pushes go directly to the bucket and downloads redirect with 307. Gogs moves to the spec response format: its per-object errors were under actions.error, which the Git LFS client never reads, so a missing object used to leave the pointer file with exit 0 and now fails with object does not exist.

@unknwon unknwon mentioned this pull request Nov 11, 2026
1 of 2 tasks
Move the transfer logic of lfsd into a separate Go module,
unknwon.dev/git-lfs-server/lfs, so that other Git servers can serve LFS
with the same storage backends. The module has an lfs.Service that
answers batch, upload, download, and verify requests on top of an
lfs.Store for metadata and a storage.Backend for content, and leaves
routing, authentication, and response encoding to the caller.

lfsd now builds one lfs.Service per forge host and keeps its HTTP
responses, except that a proxied upload of an already stored object
must send matching content before the object is linked to another
repository. Previously, any body was accepted in that case, so a
writer of one repository could gain read access to any object of
another repository by its OID and size.
Windows cannot remove the test's temp dir while the object file is open.
Add Service.Serve, which handles the batch, upload, download, and verify
requests of a repository given an Access that the caller resolved after
authenticating the request. It dispatches on the method and the last
path segment, so it works with any router that runs net/http handlers
and any URL layout. lfsd now only maps its forge permissions to an
Access and hands requests to it.

Upload actions now set Content-Type to application/octet-stream so the
Git LFS client does not send a type detected from the file content.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant